tree grammar Types;

options {
  language = Java;
  output = AST;
  tokenVocab = TreeConstruction;
  ASTLabelType = InterAST;
  filter = true;
}

@header
{
  package ru.msu.cmc.sp.oberon2;
  import java.util.List;
  import java.util.Collection;
  import java.util.Iterator;
  import java.util.LinkedList;
  import java.util.Queue;
  import ru.msu.cmc.sp.oberon2.exceptions.*;
  import ru.msu.cmc.sp.oberon2.exceptions.RecognitionError;
}


@members {
    SymbolTable symtab;
    String source;
    public Types(TreeNodeStream input, SymbolTable symtab, String src) {
        this(input);
        this.symtab = symtab;
        this.source = src;
    }
    public void reportError(RecognitionException e) throws RuntimeException
    {
       if (Oberon2.debug) throw new RuntimeException();
       System.exit(100);
    }
    Stack<Type> curReturnType = new Stack<Type>();
    Stack<InterAST> curProc = new Stack<InterAST>();
    
}

topdown  
    :   exprRoot 
    |   assignRoot
    |   proc_decl
    |   ret
    |   call_stat
    |   if_stat
    |   while_stat
    |   rep_stat
    |   for_stat
    ;

bottomup
    :   exit_proc_decl
    ;

proc_decl
   :   ^(PROC_DECL IDENT .* )
      {
          Symbol s;
          if ($IDENT.scope.getEnclosingScope() != null) s = $IDENT.scope.getEnclosingScope().resolve($IDENT.text);
          else s = $IDENT.scope.resolve($IDENT.text);
          curReturnType.push(((ProcedureType) s.type).retType);
          curProc.push($IDENT);
      }
   ;
   

exit_proc_decl
   :   ^(PROC_DECL IDENT .* )
      {
          curReturnType.pop();
          curProc.pop();
      }
   ;

ret
    : ^(q = 'RETURN' (e = expr)?)
    {
        Type t = curReturnType.peek(); 
        Type et;
        if (e == null) et = SymbolTable._void;
        else et = e.type;
        if (et == null) 
            throw new RecognitionError($q, "Unknown returning type" +
                                            " instead of " + t.getName()); 
        if (!SymbolTable.assignCompatible(t, et)) {
            throw new RecognitionError($q, "Wrong returning type: " +
                                            et.getName() + " instead of " + t.getName()); 
        }
    }
    ;

call_stat
@init{List args = new ArrayList(); List vars = new ArrayList();}
    :
    ^(CALLSTMT IDENT (^(ELIST (expr{args.add($expr.type); vars.add($expr.var);})*))?) 
    {
        Symbol s = $IDENT.scope.resolve($IDENT.text);
        Collection s1;
        if (s instanceof MethodSymbol)
        {
          if (!((ProcedureType) s.type).retType.equals(SymbolTable._void)) {
              throw new RecognitionError($IDENT, "Procedure " + $IDENT.text + " :returning value can't be ignored");    
          }
          if (((ProcedureType) s.type).params.size() != args.size()) {
              throw new RecognitionError($IDENT, "Procedure " + $IDENT.text + " :wrong number of params");
          }
          Iterator iter = ((MethodSymbol)s).getMembers().values().iterator();
          int i = 0;
          while (iter.hasNext() && i < args.size()) {
            Symbol tmp = (Symbol) iter.next();  
            Type sec = (Type)args.toArray()[i];
            if (((Boolean)vars.get(i)).equals(false) && tmp.constant == false) {
                throw new RecognitionError($IDENT,
                " Procedure " + $IDENT.text + " :Incompatible types of arguments : "  +
                "argument isn't an LValue;");
            }
            if (!SymbolTable.assignCompatible(tmp.type, sec) &&
                !(tmp.type instanceof ArrayType && sec instanceof ArrayType && 
                ((ArrayType) tmp.type).elementType.equals((((ArrayType)sec)).elementType) && 
                ((ArrayType) tmp.type).length == -1 && ((ArrayType) sec).length > 0)) 
            {
                throw new RecognitionError($IDENT,
                " Procedure " + $IDENT.text + " :Incompatible types of arguments : " + tmp.type +
                " and " + (Type)args.toArray()[i]);
            }
            i++;  
          }
        } else if (!$IDENT.text.equals("PRINT") && !$IDENT.text.equals("READ")){
                throw new RecognitionError($IDENT,
                               $IDENT.text + " : not a procedure ");
        }
        
    }
    ;

if_stat
    :   ^(IF i = expr
        {
            if (!$expr.type.equals(SymbolTable._boolean)) {
                throw new RecognitionError($IF,
                " IF " + $expr.text + " : not Boolean expression : ");
            } 
        }
        ^(SEQUENCE .*)
        (^(SEQUENCE .*) | if_stat | 'END'))
    ;

while_stat
    :   ^(WH = 'WHILE' expr ^(SEQUENCE .*))  
        {;
            if (!$expr.type.equals(SymbolTable._boolean)) {
                throw new RecognitionError($WH, 
                " WHILE " + $expr.text + " : not Boolean expression : ");
            }
        }
    ;
    
rep_stat
		:   ^(REP = 'REPEAT' expr ^(SEQUENCE .*))
		    {
            if (!$expr.type.equals(SymbolTable._boolean)) {
                throw new RecognitionError($REP,
                " REPEAT  " + $expr.text + " : not Boolean expression : ");
            }
        }
		;

for_stat
    :   ^(FO ='FOR' ^(ASSIGN IDENT f = expr) (to = expr) (by = expr)? ^(SEQUENCE .*))
        {
            Symbol s = $IDENT.scope.resolve($IDENT.text);
            if (s == null) {
                throw new RecognitionError($FO,
                " FOR " + $IDENT.text + " : scope problem : ");  
            } else if (!(s.type.isInt() && SymbolTable.assignCompatible(s.type, $f.type) 
                         && SymbolTable.assignCompatible(s.type, $to.type) 
                         && ($by.start == null || $by.start != null && 
                         SymbolTable.assignCompatible(s.type, $by.type) && $by.constant == true))) {
                throw new RecognitionError($FO,
                      $FO.text + " : parametrs problem ");
            }  
            
        }
    ;
    
assignRoot
    : ^(ASSIGN IDENT expr) 
    {
        Symbol s = $IDENT.scope.resolve($IDENT.text);
        if (s == null) { 
            throw new RecognitionError($IDENT,
                              " Unknown varabile: " + $IDENT.text);
        }
        if (s.constant) {
            throw new RecognitionError($IDENT, "Constant " 
                               + s.name + " in left part of assigment");
        }    
        if (!SymbolTable.assignCompatible(s.type, $expr.type)) {
            throw new RecognitionError($IDENT, "Incompatible types in assignment " 
                               + s.type + " and " + $expr.type);
        }
    } 
    ;


exprRoot returns [Type type, boolean constant, boolean var]
    :   ^(EXPR expr) 
    {

        $EXPR.evalType = $expr.type;
        $constant = $expr.constant;
        $var = $expr.var;
        $type = $expr.type;
    }
    ;

expr returns [Type type, boolean constant, boolean var]
@init {$constant = false; $var = false;}
@after { $start.evalType = $type;  }
    :   binaryOps   {$type = $binaryOps.type;}
    |   ^(BOOLCONST BOOLEAN)    {$type = SymbolTable._boolean; $constant = true; }  
    |   ^(CHARCONST CHAR)       {$type = SymbolTable._char; $constant = true;}
    |   ^(INTCONST INTEGER)     {$type = SymbolTable._int; $constant = true;} 
    //|   ^(BYTECONST INTEGER)    {$type = SymbolTable._byte; $constant = true;} 
    |   ^(SHORTCONST INTEGER)   {$type = SymbolTable._shortint; $constant = true;}   
    |   ^(LINTCONST INTEGER)    {$type = SymbolTable._longint; $constant = true;}  
    |   ^(REALCONST REAL)       {$type = SymbolTable._real; $constant = true;} 
    |   ^(LREALCONST REAL)      {$type = SymbolTable._longreal; $constant = true;} 
    |   ^(STRCONST STRING)      {$type = new ArrayType(SymbolTable._char, $STRING.text.length() - 2); $constant = true;}
    |   IDENT 
        {
            Scope scp = $IDENT.scope;
            if (scp == null) {
                throw new RecognitionError($IDENT,
                " Variable " + $IDENT.text + " exists without a scope");
            }
            Symbol s = scp.resolve($IDENT.text);
            if (s == null) {
                throw new RecognitionError($IDENT, 
                " Unknown variable " + $IDENT.text);
            }
            $IDENT.symbol = s; 
            if (!s.constant) $var=true;
            $type = s.type;
        }
    |   arrayRef    {$type = $arrayRef.type; $constant = $arrayRef.constant; $var = $arrayRef.var;}
    |   exprRoot    {$type = $exprRoot.type; $constant = $exprRoot.constant; $var = $exprRoot.var;}   
    |   call        {$type = $call.type;}
    ;

//It's ridiculous. Copypaste of call_stat because of Kotsynyak's stupid AST structure. No matter.
call returns [Type type]
@init { List args = new ArrayList();  List vars = new ArrayList();}
    :   ^(EXPR IDENT ^(ELIST (expr {args.add($expr.type); vars.add($expr.var);})*))
    {
        Symbol s = $IDENT.scope.resolve($IDENT.text);
        Collection s1;
        if (s instanceof MethodSymbol)
        {
          $type = ((ProcedureType) s.type).retType;
          if (((ProcedureType) s.type).params.size() != args.size()) {
              throw new RecognitionError($IDENT, "Procedure " + $IDENT.text + " :wrong number of params");
          }
          Iterator iter = ((MethodSymbol)s).getMembers().values().iterator();
          int i = 0;
          while (iter.hasNext() && i < args.size()) {
            Symbol tmp = (Symbol) iter.next();  
            Type sec = (Type)args.toArray()[i];
            if (((Boolean)vars.get(i)).equals(false) && tmp.constant == false) {
                throw new RecognitionError($IDENT,
                " Procedure " + $IDENT.text + " :Incompatible types of arguments : "  +
                "argument isn't an LValue;");
            }
            if (!SymbolTable.assignCompatible(tmp.type, sec) &&
                !(tmp.type instanceof ArrayType && sec instanceof ArrayType && 
                ((ArrayType) tmp.type).elementType.equals((((ArrayType)sec)).elementType) && 
                ((ArrayType) tmp.type).length == -1 && ((ArrayType) sec).length > 0)) 
            {
                throw new RecognitionError($IDENT,
                " Procedure " + $IDENT.text + " :Incompatible types of arguments : " + tmp.type +
                " and " + (Type)args.toArray()[i]);
            }
            i++;  
          }
        } else if (!$IDENT.text.equals("PRINT") && !$IDENT.text.equals("READ")){
                throw new RecognitionError($IDENT,
                               $IDENT.text + " : not a procedure ");
        }
        
    }
    
    ;

binaryOps returns [Type type]
@after { $start.evalType = $type; }
    :   
        ^(blop a=expr b=expr)   
        {
            if (!($a.type.equals(SymbolTable._boolean) && $b.type.equals(SymbolTable._boolean))){
                throw new RecognitionError(0,0, 
                " BLOP  " + " : type mismatch : " + $a.type +
                " and " + $b.type);
            }
            $type = SymbolTable._boolean;
        }
    |   ^(nop a=expr b=expr)
        {
            $type = SymbolTable.ariphmCompatible($a.type, $b.type);{
                
            if ($type == null) {
                throw new RecognitionError(0,0,
                " NOP " + " : type mismatch : " + $a.type +
                " and " + $b.type);
            }
            }
        }
    |   ^(slash a=expr b=expr)
        {
            $type = SymbolTable.ariphmCompatible($a.type, $b.type);
            if ($type == null){
                throw new RecognitionError(0,0,
                " SLASH " + " : type mismatch : " + $a.type +
                " and " + $b.type);
             }
            if ($type.isInt()) $type = SymbolTable._real;
        }
    |   ^( iop a=expr b=expr)
        {
            $type = SymbolTable.ariphmCompatible($a.type, $b.type);
            if ($type == null || !$a.type.isInt() || !$b.type.isInt()){
                throw new RecognitionError(0,0,
                "  " + " : type mismatch : " + $a.type +
                " and " + $b.type);
                }
        }
    |   ^(relop a=expr b=expr) 
        {         
            if (SymbolTable.ariphmCompatible($a.type, $b.type) != null ||
                $a.type.equals(SymbolTable._char) && $b.type.equals(SymbolTable._char))
                $type = SymbolTable._boolean;
            else {
                throw new RecognitionError(0,0,
                " RELOP  " + " : type mismatch : " + $a.type +
                " and " + $b.type);
            }
        }
    |   ^(  eqop a=expr b=expr)  
        {
          if (SymbolTable.assignCompatible($a.type, $b.type) || SymbolTable.assignCompatible($b.type, $a.type) ||
              ($a.type instanceof ArrayType && $b.type instanceof ArrayType && 
              ((ArrayType)$a.type).elementType.equals((((ArrayType)$b.type).elementType)) &&
              ((ArrayType)$a.type).elementType.equals(SymbolTable._char) &&
              ((ArrayType)$a.type).length > 0 && ((ArrayType)$b.type).length > 0))
              $type=SymbolTable._boolean;
          else {
                throw new RecognitionError(0,0, $eqop.text + 
                " EQOP " + " : type mismatch : " + $a.type +
                " and " + $b.type);
          }
        }
    ;

arrayRef returns [Type type, boolean constant, boolean var]
    :   ^(INDEX IDENT expr)
        {
            if (!$expr.type.isInt()) throw new RecognitionError($INDEX, "Array ref is not int");
            $type = symtab.arrayIndex($IDENT, $expr.start);
            if ($expr.start.symbol != null && $expr.start.symbol.constant) { $constant = true; $var = false;}
            else {$constant = false; $var = true;}
            $start.evalType = $type;
        }
    ;

nop   :   PLUS | MINUS | STAR;

slash :   SLASH;

iop   :   'DIV' | 'MOD';

blop  :   AMP | TILDE | 'OR';

relop :   LESS | GREATER | LEQ | GEQ ;

eqop  :   NEQUAL | EQUAL ;
